#!/usr/bin/env bash
# 
# clean4llm(Clean for LLM)，项目代码添加到知识库的辅助工具
# 实现将指定本地文件夹(多数情况下是git代码仓库)递归复制所有源码文件到新文件夹，并排除指定文件二进制文件
# 可以通过修改 FILE_EXCLUDE_RULES 和 FILE_EXCLUDE_RULES_EXCLUDE 两个变量自定义排除规则

# 输出错误信息并退出
set -eo pipefail
# 错误退出时输出错误信息
trap 'echo "错误：在行 ${LINENO} 执行命令失败 - 命令: $BASH_COMMAND, 退出码: $?" >&2' ERR
# 输出错误信息中记录完整调用栈
# trap 'echo "错误追踪：
#   文件: ${BASH_SOURCE[0]}
#   行号: ${LINENO}
#   调用栈: ${FUNCNAME[*]}
#   失败命令: $BASH_COMMAND
#   退出码: $?" >&2' ERR

# 显示帮助信息
function show_help() {
    cat <<EOF
clean4llm - 代码仓库清理工具 v1.0

用途：清理并复制代码仓库到指定目录，排除二进制文件

使用方法：
  $0 [选项] [源目录]

选项：
  -v             显示详细输出
  --output=目录   指定输出目录（默认：./源目录名_src）
  -h
  --help         显示帮助信息

示例：
  # 清理当前git仓库到默认输出目录
  $0 

  # 指定源目录和输出目录
  $0 /path/to/src --output=~/cleaned_code

文件筛选：
  • 自动排除二进制文件（如exe, jar, png等）
  • 自动排除版本控制目录（.git等）
  • 支持.gitignore规则
EOF
}

# 默认不输出详细日志
VERBOSE=
# 源目录
SOURCE_DIR=""
# 目标目录
TARGET_DIR=""
# 参数解析
while [[ $# -gt 0 ]]; do
    case "$1" in
        -v|--verbose)
            VERBOSE=1
            shift
            ;;
        --output)
            TARGET_DIR="$2"
            shift 2
            ;;
        --output=*)
            TARGET_DIR="${1#*=}"
            shift
            ;;
        -h|--help)
            show_help
            exit 0
            ;;
        *)
            # 增加对无效参数的检测
            if [[ "$1" == -* ]]; then
                echo "错误：无效参数 '$1'" >&2
                show_help
                exit 1
            fi
            SOURCE_DIR="$1"
            shift
            ;;
    esac
done

if [[ -z "$SOURCE_DIR" ]]; then
    # 检测当前目录是否有.git文件夹
    if [[ -d ".git" ]]; then
        SOURCE_DIR=$(pwd)
        [[ -n "$VERBOSE" ]] && echo "检测到.git目录，自动设置源目录为当前目录: $SOURCE_DIR"
    fi
fi
# 将SOURCE_DIR 转为全路径
if [[ -n "$SOURCE_DIR" ]]; then
    SOURCE_DIR=$(cd -- "$SOURCE_DIR" && pwd)
fi
# 校验源目录
if [[ ! -d "$SOURCE_DIR" ]]; then
    echo "错误：请指定有效源目录，或确保当前目录包含.git文件夹" >&2
    exit 1
fi

# 设置目标目录
if [[ -z "$TARGET_DIR" ]]; then
    SOURCE_BASENAME=$(basename "$SOURCE_DIR")
    TARGET_DIR="$(pwd)/${SOURCE_BASENAME}_src"
fi

# 清理并创建目标目录
rm -rf "$TARGET_DIR"
mkdir -p "$TARGET_DIR"

TARGET_DIR=$(cd -- "$TARGET_DIR" && pwd)

# 文件排除规则
declare -a FILE_EXCLUDE_RULES=(
    # 强制忽略的二进制文件类型
    '*.exe' '*.bin' '*.so' '*.pyd'
    '*.doc' '*.xls' '*.ppt'
    '*.a' '*.lib' '*.o' '*.obj'
    '*.class' '*.prefs'
    # 强制忽略 java 打包文件
    '*.jar' '*.war' '*.ear'
    # 强制忽略压缩包
    '*.zip' '*.rar' '*.7z' '.bz2' '*.iso' '*.tar' '*.gz' '*.tgz'
    '*.xz' '*.z' '*.arj' '*.cab' '*.lzh' '*.ex_' '*.dl_'
    '*.log' 
    # 强制忽略图像格式文件
    '*.png' '*.jpg' '*.jpeg' '*.gif' '*.bmp' '*.tiff' '*.tif' '*.webp' '*.psd' '*.raw'
    '*.heic' '*.pcx' '*.tga' '*.jp2' '*.j2k' '*.iff' '*.fpx' 
    # 强制忽略矢量图像格式
    '*.svg' '*.ai' '*.eps' 
    '*.cdr' '*.dwg' '*.dxf' '*.ico' '*.xbm' '*.xpm'
    # 强制忽略医学专用图像文件
    '*.dcm' '*.nii' '*.img' '*.hdr' '*.nrrd' '*.mnc' '*.par' '*.rec' '*.svs'
    # 强制忽略视频格式文件
    '*.mp4' '*.avi' '*.mkv' '*.mov' '*.flv' '*.wmv' '*.rmvb' '*.3gp' 
    '*.webm' '*.ts' '*.m2ts' '*.qlv' '*.vob' '*.asf'
    # 强制忽略音频格式文件
    '*.wav' '*.mp3' '*.flac' '*.aac' '*aif' '*.aiff' '*.ape' '*.ogg' '*.wma'
    '*.mid' '*.midi' '*.cda' '*.ra' '*.rm' '*.au' '*.vqf' '*.dsf' '*.dff'
    # 强制忽略数据库文件
    '*.rdb' '*.aof' '*.db' '*.mdf' '*.ndf' '*.ldf' '*.frm' '*.ibd' '*.myd' '*.myi' '*.dbf'
    '*.ctl' '*.dmp' '*.dat' '*.backup' '*.sqlite' '*.mdb' '*.accdb' '*.bson' '*.ibdata'
    '*.fdb' '*.hdb' '*.backup'
    # 强制忽略二进制模型文件
    '*.gguf' '*.pt' '*.pth' '*.ckpt' '*.onnx' '*.safetensors'
    
    # 特殊文件模式 
    '.*' '~*' 
)

# 文件夹排除规则
PATH_EXCLUDE_RULES=(
    # 强制忽略的目录
    'bin/' 'build/' 'target/' 'lib/' 'log/' 'release/' '.*/'
)

# 构建排除表达式
EXCLUDE_PATTERN=()
# 1. 路径排除（目录级别）
for _dir in "${PATH_EXCLUDE_RULES[@]}"; do
    _dir="${_dir#/}"    # 删除开头的/
    _dir="${_dir%/}"    # 删除结尾的/
    EXCLUDE_PATTERN+=( -path "*/$_dir/*" -prune -o )
done
# 2. 文件排除（文件级别）
if [ "${#FILE_EXCLUDE_RULES[@]}" -gt 0 ]; then
    EXCLUDE_PATTERN+=( \( -type f \( )
    for _ext in "${FILE_EXCLUDE_RULES[@]}"; do
        EXCLUDE_PATTERN+=( -name "$_ext" -prune -o )
    done
    EXCLUDE_PATTERN=("${EXCLUDE_PATTERN[@]::${#EXCLUDE_PATTERN[@]}-1}")
    EXCLUDE_PATTERN+=( \) \) -o )
fi

# 预计算是否启用git check-ignore
use_git_ignore=0
if [ -f "$SOURCE_DIR/.gitignore" ] && command -v git &>/dev/null; then
    use_git_ignore=1
fi

# 生成临时文件列表（同时处理.gitignore过滤）
echo -e "\033[34m统计文件数量。。。\033[0m"
START_TIME=$(date +%s)
tmp_list=$(mktemp)
# 待复制文件总数
total=0

{
    while IFS= read -r -d '' file; do
        [ "$use_git_ignore" -eq 1 ] && 
            git -C "$SOURCE_DIR" check-ignore -q "${file#$SOURCE_DIR/}" && continue
        echo "$file"
        total=$((total + 1))
    done < <(find "$SOURCE_DIR" "${EXCLUDE_PATTERN[@]}" -type f -print0)

} > "$tmp_list"

if [[ $total -eq 0 ]]; then
    echo -e "\033[33m没有发现需要复制的文件\033[0m"
    rm -f "$tmp_list"
    exit 0
fi

if (( VERBOSE )); then
    echo -e "\033[33m要复制的文件列表：\033[0m"  # 黄色高亮
    while IFS= read -r file; do
        echo "-  $file"
    done < "$tmp_list"
fi

echo "开始复制 $SOURCE_DIR 的 $total 个文件"
echo # 增加空行用于光标操作

# 统计文件类型
declare -A file_stats=()
# 复制文件计数器
count=0

while IFS= read -r file; do
    rel_path="${file#$SOURCE_DIR/}"
    dest="$TARGET_DIR/$rel_path"
    
    mkdir -p "$(dirname "$dest")"
    cp "$file" "$dest"
    
    filename=$(basename "$rel_path")
    
    # 后缀判定逻辑
    if [[ "$filename" == *.* ]]; then
        # 处理正常前缀+后缀或纯后缀类型
        if [[ "$filename" =~ ^\..+$ && $(tr -dc '.' <<< "$filename" | wc -c) -eq 1 ]]; then
            # 纯后缀类型（如 .bashrc）
            key="$filename"
        else
            # 正常前后缀组合（如 file.txt）
            suffix="${filename##*.}"
            key="*.$suffix"
        fi
    else
        # 无后缀类型（如 LICENSE）
        key="$filename"
    fi
    if [[ ! "${file_stats[$key]}" ]]; then
        file_stats["$key"]=0
    fi
    file_stats["$key"]=$(( file_stats["$key"] + 1))  # 统一计数加1

    count=$((count + 1))
    percent=$((count * 100 / total))
    cols=$(tput cols)
    bar_len=$((cols - 20))
    filled=$((bar_len * count / total))
    
    # 处理文件名折叠
    max_path_len=$((cols - 12))  # 前缀"当前文件: "占12字符宽度
    if [[ ${#rel_path} -gt $max_path_len ]]; then
        folded_length=$((max_path_len - 3))
        front_length=$((folded_length / 2))
        end_length=$((folded_length - front_length))
        front_part="${rel_path:0:front_length}"
        end_part="${rel_path: -end_length}"
        folded_path="${front_part}...${end_part}"
    else
        folded_path="$rel_path"
    fi
    
    # 光标控制：覆盖前一次输出
    if [[ $count -gt 1 ]]; then
        printf "\033[2A"  # 上移两行
    fi
    # 清除行并输出进度条
    printf "\033[K|%-*s| %3d%% (%d/%d)\n" \
        "$bar_len" \
        "$(printf '#%.0s' $(seq 1 "$filled"))" \
        "$percent" \
        "$count" \
        "$total"
    # 清除行并输出文件名
    printf "\033[K当前文件: %s\n" "$folded_path"   

done < "$tmp_list"

echo # 换行保证输出整洁

rm -f "$tmp_list"
echo -e "\033[32m复制完成 源目录已复制到\033[0m $TARGET_DIR"

# 输出统计信息
echo -e "===== 统计信息 ====="
if [[ ${#file_stats[@]} -gt 0 ]]; then
    {
        echo  "类型 数量"  # 手动添加表头
        for key in "${!file_stats[@]}"; do
            echo "$key ${file_stats[$key]}"
        done

    } | column -t
else
    echo "无"
fi
echo "耗时: $(($(date +%s) - $START_TIME))秒"